package ly.count.ohos.sdk;


import ohos.app.Context;
import ohos.data.DatabaseHelper;
import ohos.data.preferences.Preferences;
import org.json.JSONException;
import org.json.JSONObject;

import java.util.*;

public class CountlyStore {
    private static final String PREFERENCES = "COUNTLY_STORE";
    private static final String PREFERENCES_PUSH = "ly.count.ohos.api.messaging";
    private static final String DELIMITER = ":::";
    private static final String CONNECTIONS_PREFERENCE = "CONNECTIONS";
    private static final String EVENTS_PREFERENCE = "EVENTS";
    private static final String STAR_RATING_PREFERENCE = "STAR_RATING";
    private static final String CACHED_ADVERTISING_ID = "ADVERTISING_ID";
    private static final String REMOTE_CONFIG_VALUES = "REMOTE_CONFIG";
    private static final String CACHED_PUSH_ACTION_ID = "PUSH_ACTION_ID";
    private static final String CACHED_PUSH_ACTION_INDEX = "PUSH_ACTION_INDEX";
    private static final String CACHED_PUSH_MESSAGING_MODE = "PUSH_MESSAGING_MODE";
    private static final String CACHED_PUSH_MESSAGING_PROVIDER = "PUSH_MESSAGING_PROVIDER";
    private static final int MAX_EVENTS = 100;
    static int MAX_REQUESTS = 10;//value is configurable for tests

    private final Preferences preferences_;
    private final Preferences preferencesPush_;
    private final StringPreferences stringPreferences;

    private static final String CONSENT_GCM_PREFERENCES = "ly.count.ohos.api.messaging.consent.gcm";

    ModuleLog L;

    /**
     * Constructs a CountlyStore object.
     *
     * @param context used to retrieve storage meta data, must not be null.
     * @param logModule
     */
    CountlyStore(final Context context, ModuleLog logModule) {
        if (context == null) {
            throw new IllegalArgumentException("must provide valid context");
        }
        preferences_ = new DatabaseHelper(context).getPreferences(PREFERENCES);
        stringPreferences = StringPreferences.getInstance(context);
        preferencesPush_ = createPreferencesPush(context);
        L = logModule;
    }

    static Preferences createPreferencesPush(Context context) {
        return new DatabaseHelper(context).getPreferences(PREFERENCES_PUSH);
    }

    /**
     * Returns an unsorted array of the current stored connections.
     *
     * @return String[]
     */
    public String[] connections() {
//        final String joinedConnStr = preferences_.getString(CONNECTIONS_PREFERENCE, "");
        final String joinedConnStr = stringPreferences.getString(CONNECTIONS_PREFERENCE, "");
        return joinedConnStr.length() == 0 ? new String[0] : joinedConnStr.split(DELIMITER);
    }

    /**
     * Returns an unsorted array of the current stored event JSON strings.
     *
     * @return an unsorted array of the current stored event JSON strings
     */
    public String[] events() {
//        final String joinedEventsStr = preferences_.getString(EVENTS_PREFERENCE, "");
        final String joinedEventsStr = stringPreferences.getString(EVENTS_PREFERENCE, "");
        return joinedEventsStr.length() == 0 ? new String[0] : joinedEventsStr.split(DELIMITER);
    }

    /**
     * Returns a list of the current stored events, sorted by timestamp from oldest to newest.
     *
     * @return eventsList
     */
    public List<Event> eventsList() {
        final String[] array = events();
        final List<Event> events = new ArrayList<>(array.length);
        for (String s : array) {
            try {
                final Event event = Event.fromJSON(new JSONObject(s));
                if (event != null) {
                    events.add(event);
                }
            } catch (JSONException ignored) {
                // should not happen since JSONObject is being constructed from previously stringified JSONObject
                // events -> json objects -> json strings -> storage -> json strings -> here
            }
        }
        // order the events from least to most recent
        Collections.sort(events, new Comparator<Event>() {
            @Override
            public int compare(final Event e1, final Event e2) {
                return (int) (e1.timestamp - e2.timestamp);
            }
        });
        return events;
    }

    /**
     * Returns true if no connections are current stored, false otherwise.
     *
     * @return isEmptyConnections
     */
    public boolean isEmptyConnections() {
//        return preferences_.getString(CONNECTIONS_PREFERENCE, "").length() == 0;
        return stringPreferences.getString(CONNECTIONS_PREFERENCE, "").length() == 0;
    }

    /**
     * Adds a connection to the local store.
     *
     * @param str the connection to be added, ignored if null or empty
     */
    public synchronized void addConnection(final String str) {
        if (str != null && str.length() > 0) {
            final List<String> connections = new ArrayList<>(Arrays.asList(connections()));
            if (connections.size() < MAX_REQUESTS) {
                //request under max requests, add as normal
                connections.add(str);
//                preferences_.putString(CONNECTIONS_PREFERENCE, join(connections, DELIMITER)).flush();
                stringPreferences.putString(CONNECTIONS_PREFERENCE, join(connections, DELIMITER)).flush();
            } else {
                //reached the limit, start deleting oldest requests
                L.w("[CountlyStore] Store reached it's limit, deleting oldest request");

                deleteOldestRequest();
                addConnection(str);
            }
        }
    }

    synchronized void deleteOldestRequest() {
        final List<String> connections = new ArrayList<>(Arrays.asList(connections()));
        connections.remove(0);
//        preferences_.putString(CONNECTIONS_PREFERENCE, join(connections, DELIMITER)).flush();
        stringPreferences.putString(CONNECTIONS_PREFERENCE, join(connections, DELIMITER)).flush();
    }

    /**
     * Removes a connection from the local store.
     *
     * @param str the connection to be removed, ignored if null or empty,
     * or if a matching connection cannot be found
     */
    public synchronized void removeConnection(final String str) {
        if (str != null && str.length() > 0) {
            final List<String> connections = new ArrayList<>(Arrays.asList(connections()));
            if (connections.remove(str)) {
//                preferences_.putString(CONNECTIONS_PREFERENCE, join(connections, DELIMITER)).flush();
                stringPreferences.putString(CONNECTIONS_PREFERENCE, join(connections, DELIMITER)).flush();
            }
        }
    }

    protected synchronized void replaceConnections(final String[] newConns) {
        if (newConns != null) {
            final List<String> connections = new ArrayList<>(Arrays.asList(newConns));
            replaceConnectionsList(connections);
        }
    }

    protected synchronized void replaceConnectionsList(final List<String> newConns) {
        if (newConns != null) {
//            preferences_.putString(CONNECTIONS_PREFERENCE, join(newConns, DELIMITER)).flush();
            stringPreferences.putString(CONNECTIONS_PREFERENCE, join(newConns, DELIMITER)).flush();
        }
    }

    /**
     * Adds a custom event to the local store.
     *
     * @param event event to be added to the local store, must not be null
     */
    void addEvent(final Event event) {
        final List<Event> events = eventsList();
        if (events.size() < MAX_EVENTS) {
            events.add(event);
//            preferences_.putString(EVENTS_PREFERENCE, joinEvents(events, DELIMITER)).flush();
            stringPreferences.putString(EVENTS_PREFERENCE, joinEvents(events, DELIMITER)).flush();
        }
    }

    /**
     * Set the preferences that are used for the star rating
     * @param prefs
     */
    void setStarRatingPreferences(String prefs) {
        preferences_.putString(STAR_RATING_PREFERENCE, prefs).flush();
    }

    /**
     * Get the preferences that are used for the star rating
     * @return  StarRatingPreferences
     */
    String getStarRatingPreferences() {
        return preferences_.getString(STAR_RATING_PREFERENCE, "");
    }

    void setRemoteConfigValues(String values) {
        preferences_.putString(REMOTE_CONFIG_VALUES, values).flush();
    }

    String getRemoteConfigValues() {
        return preferences_.getString(REMOTE_CONFIG_VALUES, "");
    }

    void setCachedAdvertisingId(String advertisingId) {
        preferences_.putString(CACHED_ADVERTISING_ID, advertisingId).flush();
    }

    String getCachedAdvertisingId() {
        return preferences_.getString(CACHED_ADVERTISING_ID, "");
    }

    void setConsentPush(boolean consentValue) {
        preferencesPush_.putBoolean(CONSENT_GCM_PREFERENCES, consentValue).flush();
    }

    Boolean getConsentPush() {
        return preferencesPush_.getBoolean(CONSENT_GCM_PREFERENCES, false);
    }

    public static Boolean getConsentPushNoInit(Context context) {
        Preferences sp = createPreferencesPush(context);
        return sp.getBoolean(CONSENT_GCM_PREFERENCES, false);
    }

    /**
     * Adds a custom event to the local store.
     *
     * @param key name of the custom event, required, must not be the empty string
     * @param segmentation segmentation values for the custom event, may be null
     * @param segmentationInt
     * @param segmentationDouble
     * @param segmentationBoolean
     * @param timestamp timestamp (seconds since 1970) in GMT when the event occurred
     * @param hour current local hour on device
     * @param dow current day of the week on device
     * @param count count associated with the custom event, should be more than zero
     * @param sum sum associated with the custom event, if not used, pass zero.
     * @param dur double
     * NaN and infinity values will be quietly ignored.
     */
    public synchronized void addEvent(final String key, final Map<String, String> segmentation, final Map<String, Integer> segmentationInt, final Map<String, Double> segmentationDouble, final Map<String, Boolean> segmentationBoolean,
                                      final long timestamp, final int hour, final int dow, final int count, final double sum, final double dur) {
        final Event event = new Event();
        event.key = key;
        event.segmentation = segmentation;
        event.segmentationDouble = segmentationDouble;
        event.segmentationInt = segmentationInt;
        event.segmentationBoolean = segmentationBoolean;
        event.timestamp = timestamp;
        event.hour = hour;
        event.dow = dow;
        event.count = count;
        event.sum = sum;
        event.dur = dur;

        addEvent(event);
    }

    /**
     * Removes the specified events from the local store. Does nothing if the event collection
     * is null or empty.
     *
     * @param eventsToRemove collection containing the events to remove from the local store
     */
    public synchronized void removeEvents(final Collection<Event> eventsToRemove) {
        if (eventsToRemove != null && eventsToRemove.size() > 0) {
            final List<Event> events = eventsList();
            if (events.removeAll(eventsToRemove)) {
//                preferences_.putString(EVENTS_PREFERENCE, joinEvents(events, DELIMITER)).flush();
                stringPreferences.putString(EVENTS_PREFERENCE, joinEvents(events, DELIMITER)).flush();
            }
        }
    }

    /**
     * Converts a collection of Event objects to URL-encoded JSON to a string, with each
     * event JSON string delimited by the specified delimiter.
     *
     * @param collection events to join into a delimited string
     * @param delimiter delimiter to use, should not be something that can be found in URL-encoded JSON string
     * @return StringBuilder
     */
    @SuppressWarnings("SameParameterValue")
    static String joinEvents(final Collection<Event> collection, final String delimiter) {
        final List<String> strings = new ArrayList<>();
        for (Event e : collection) {
            strings.add(e.toJSON().toString());
        }
        return join(strings, delimiter);
    }

    public static synchronized void cachePushData(String id_key, String index_key, Context context) {
        Preferences sp = createPreferencesPush(context);
        sp.putString(CACHED_PUSH_ACTION_ID, id_key).flush();
        sp.putString(CACHED_PUSH_ACTION_INDEX, index_key).flush();
    }

    String[] getCachedPushData() {
        String[] res = new String[2];
        res[0] = preferencesPush_.getString(CACHED_PUSH_ACTION_ID, null);
        res[1] = preferencesPush_.getString(CACHED_PUSH_ACTION_INDEX, null);
        return res;
    }

    void clearCachedPushData() {
        preferencesPush_.delete(CACHED_PUSH_ACTION_ID).flush();
        preferencesPush_.delete(CACHED_PUSH_ACTION_INDEX).flush();
    }

    public static void cacheLastMessagingMode(int mode, Context context) {
        Preferences sp = createPreferencesPush(context);
        sp.putInt(CACHED_PUSH_MESSAGING_MODE, mode).flush();
    }

    public static int getLastMessagingMode(Context context) {
        Preferences sp = createPreferencesPush(context);
        return sp.getInt(CACHED_PUSH_MESSAGING_MODE, -1);
    }

    public static void storeMessagingProvider(int provider, Context context) {
        Preferences sp = createPreferencesPush(context);
        sp.putInt(CACHED_PUSH_MESSAGING_PROVIDER, provider).flush();
    }

    public static int getMessagingProvider(Context context) {
        Preferences sp = createPreferencesPush(context);
        return sp.getInt(CACHED_PUSH_MESSAGING_PROVIDER, 0);
    }

    /**
     * Joins all the strings in the specified collection into a single string with the specified delimiter.
     *
     * @param collection Collection<String>
     * @param delimiter String
     * @return StringBuilder
     */
    static String join(final Collection<String> collection, final String delimiter) {
        final StringBuilder builder = new StringBuilder();

        int i = 0;
        for (String s : collection) {
            builder.append(s);
            if (++i < collection.size()) {
                builder.append(delimiter);
            }
        }
        return builder.toString();
    }

    /**
     * Retrieves a preference from local store.
     *
     * @param key the preference key
     * @return value
     */
    public synchronized String getPreference(final String key) {
        return preferences_.getString(key, null);
    }

    /**
     * Adds a preference to local store.
     *
     * @param key the preference key
     * @param value the preference value, supply null value to remove preference
     */
    public synchronized void setPreference(final String key, final String value) {
        if (value == null) {
            preferences_.delete(key).flush();
        } else {
            preferences_.putString(key, value).flush();
        }
    }

    // for unit testing
    synchronized void clear() {
//        preferences_.delete(EVENTS_PREFERENCE);
        stringPreferences.delete(EVENTS_PREFERENCE);
//        preferences_.delete(CONNECTIONS_PREFERENCE);
        stringPreferences.delete(CONNECTIONS_PREFERENCE);
        preferences_.clear();
        preferences_.flush();

        preferencesPush_.clear().flush();
    }
}
